Skip to content

fix: ArrayDescriber splitting arrayKey() union into anyOf [array, object]#2704

Open
gnutix wants to merge 2 commits intonelmio:5.xfrom
gnutix:fix/array-describer-arraykey-union
Open

fix: ArrayDescriber splitting arrayKey() union into anyOf [array, object]#2704
gnutix wants to merge 2 commits intonelmio:5.xfrom
gnutix:fix/array-describer-arraykey-union

Conversation

@gnutix
Copy link
Copy Markdown

@gnutix gnutix commented Mar 4, 2026

Problem

When Symfony's TypeInfo resolves array<T> (single generic parameter, no explicit key type), the key defaults to arrayKey() — a union(int, string). ArrayDescriber splits this into separate array<int, T> and array<string, T> types, producing an incorrect OpenAPI schema:

{
  "anyOf": [
    { "type": "array", "items": { "$ref": "#/components/schemas/MyDto" } },
    { "type": "object", "additionalProperties": { "$ref": "#/components/schemas/MyDto" } }
  ]
}

The correct output should be just { "type": "array", "items": { "$ref": "#/components/schemas/MyDto" } }, since array<T> in PHP — especially in the context of JSON API responses — means a sequential array (JSON array), not "either array or object".

Traversable objects as generic parameters

When a Traversable object is used as a generic parameter (e.g. list<MyIterableClass>), StringTypeResolver wraps it in CollectionType(ObjectType) with no key/value type info. ArrayDescriber then describes this as array<unknown> (because getCollectionValueType() returns mixed()), losing the type reference entirely.

The fix detects when the wrapped type is an ObjectType (meaning StringTypeResolver auto-wrapped a Traversable class) and delegates to the chain so ClassDescriber can create a proper $ref.

Reproduction

final class EventDTO
{
    /** @var RefereeDTO[] */
    public array $referees;
}

With type_info: true (Symfony ≥ 7.3), the referees property schema includes both array and object types instead of just array.

Relation to #2508

Issue #2508 reported two symptoms:

  1. type_info: false: "items": {} (empty, no type info) — this was caused by ConstructorExtractor not reading @var on promoted constructor properties, and was fixed upstream in Symfony 7.4 ([Serialization] with_constructor_extractor: true - @var docs on promoted properties ignored symfony/symfony#60795).
  2. type_info: true: "oneOf": [{ "type": "array" }, { "type": "object" }] — this is the ArrayDescriber issue fixed by this PR. Even with Symfony 7.4, once TypeInfo correctly reads the type, ArrayDescriber still splits the arrayKey() union into anyOf: [array, object].

This PR fixes the remaining symptom 2 from #2508.

Copilot AI review requested due to automatic review settings March 4, 2026 14:39
@gnutix gnutix force-pushed the fix/array-describer-arraykey-union branch from 1a128c9 to 3fb5d91 Compare March 4, 2026 14:42
@gnutix gnutix changed the title Fix ArrayDescriber splitting arrayKey() union into anyOf [array, object] fix: ArrayDescriber splitting arrayKey() union into anyOf [array, object] Mar 4, 2026
@gnutix gnutix force-pushed the fix/array-describer-arraykey-union branch from 3fb5d91 to c471c6b Compare March 4, 2026 14:44
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes how ArrayDescriber handles Symfony TypeInfo collection key unions so that array<T> (with the implicit arrayKey(): int|string key) is described as a JSON array rather than being split into an OpenAPI anyOf/oneOf of [array, object]. It also improves handling of Traversable objects that TypeInfo auto-wraps as collections, ensuring object references aren’t lost.

Changes:

  • Treat arrayKey() (int|string) key unions as a list/JSON array instead of splitting into separate keyed array types.
  • Unwrap CollectionType(ObjectType) cases (auto-wrapped Traversable objects) and delegate directly so the $ref can be produced.
  • Add PHPUnit coverage for both behaviors.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
src/TypeDescriber/ArrayDescriber.php Adds special-casing for arrayKey() unions and unwraps Traversable object collections to preserve $ref types.
tests/TypeDescriber/ArrayDescriberTest.php Adds tests to validate list handling for arrayKey() unions and unwrapping behavior for Traversable objects.

You can also share your feedback on Copilot code review. Take the survey.

Comment thread tests/TypeDescriber/ArrayDescriberTest.php
Comment thread tests/TypeDescriber/ArrayDescriberTest.php
Comment thread src/TypeDescriber/ArrayDescriber.php
@gnutix gnutix force-pushed the fix/array-describer-arraykey-union branch from c471c6b to 23dc4ee Compare March 4, 2026 15:11
@codecov
Copy link
Copy Markdown

codecov Bot commented Mar 4, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.41%. Comparing base (7f69a49) to head (f8a616a).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##              5.x    #2704      +/-   ##
==========================================
- Coverage   95.59%   95.41%   -0.18%     
==========================================
  Files          94       94              
  Lines        3064     3078      +14     
==========================================
+ Hits         2929     2937       +8     
- Misses        135      141       +6     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@gnutix gnutix force-pushed the fix/array-describer-arraykey-union branch from 645c11d to 28ef80c Compare March 4, 2026 15:34
gnutix and others added 2 commits March 4, 2026 16:40
When TypeInfo resolves `array<T>` (single generic parameter, no explicit
key type), the key defaults to `arrayKey()` which is `union(int, string)`.
ArrayDescriber was splitting this into separate `array<int, T>` and
`array<string, T>` types, producing `anyOf: [{type: array}, {type: object}]`
in the OpenAPI schema. This is incorrect — `array<T>` in PHP means a
JSON array, not "either array or object".

Additionally, when a Traversable object is used as a generic parameter
(e.g. `list<MyIterableClass>`), StringTypeResolver wraps it in
CollectionType(ObjectType) with no key/value type info. ArrayDescriber
then describes this as `array<unknown>`. The fix detects when the wrapped
type is an ObjectType and delegates to the chain so ClassDescriber can
create a proper `$ref`.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@gnutix gnutix force-pushed the fix/array-describer-arraykey-union branch from 28ef80c to f8a616a Compare March 4, 2026 15:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants